iT邦幫忙

2026 iThome 鐵人賽

DAY 9
0
IT Operation

系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server系列 第 9

Day 9|Windows Event Log 自動巡檢:不要等使用者報錯才去翻事件檢視器

  • 分享至 

  • xImage
  •  

前面幾天,我們已經讓 PowerShell 可以檢查:

CPU
Memory
Disk
Service
Uptime
Last Boot Time

到 Day 8,已經可以大概知道:

這台 Server 現在的資源與重要 Service 看起來是否正常?

但實際做 Windows 維運時,常常會遇到另一種情況:

CPU 正常
Memory 正常
Disk 正常
Service 也是 Running

使用者卻還是說:

「系統剛剛有問題。」

這時候我通常還會去看另一個地方:

Windows Event Log。

也就是大家很熟悉的:

Event Viewer
事件檢視器

以前可能是:

Win + R

eventvwr.msc

Windows Logs

System / Application

Filter Current Log

慢慢找 Error

今天我們就把這個流程交給 PowerShell。

目標是:

Windows Event Log

只找最近 24 小時

Critical / Error / Warning

整理 Event ID

找出重複發生的事件

輸出 CSV
為什麼 Event Log 值得自動化?

Event Viewer 本身其實很好用。

如果只是偶爾排查單一問題,我還是會直接打開:

eventvwr.msc

但假設每天都要巡檢:

SERVER01
SERVER02
SERVER03
SERVER04
...

然後每一台都:

開 Event Viewer
點 System
Filter Error
看最近一天

這就開始變成很適合自動化的工作。

尤其 Event Log 最大的問題不是「沒有資料」。

而是:

資料太多。

一台長期運作的 Windows Server,可能累積大量:

Information
Warning
Error
Critical

我們真正要做的是:

從很多 Event 裡,快速找出值得進一步確認的東西。

今天主要使用 Get-WinEvent

PowerShell 可以透過:

Get-WinEvent

讀取 Windows Event Log。

先看看有哪些 Log:

Get-WinEvent -ListLog *

通常會看到非常多項目。

例如:

Application
Security
Setup
System
Windows PowerShell
Microsoft-Windows-...

今天先不要一次處理全部。

我們先鎖定最常用的兩個:

System
Application
先讀 System Log

最直接可以:

Get-WinEvent -LogName System

但不建議直接這樣跑完整紀錄。

因為可能非常多。

先只看最近 20 筆:

Get-WinEvent -LogName System
-MaxEvents 20

會看到類似:

TimeCreated Id LevelDisplayName ProviderName


2026/09/17 05:30 7036 Information Service Control Manager
2026/09/17 05:28 10016 Warning DistributedCOM
2026/09/17 05:20 6005 Information EventLog

這裡幾個很重要的欄位:

TimeCreated
Id
LevelDisplayName
ProviderName
Message

後面我們產生報表主要就是看這些。

Event ID 是什麼?

每一個 Windows Event 通常都會有:

Event ID

例如:

7036
6005
6006
41
10016

它可以幫助我們判斷:

到底是哪一類事件。

不過要注意:

只看 Event ID 不一定足夠。

因為不同 Provider 可能使用相同的 Event ID。

所以排查時最好一起看:

ProviderName
+
Event ID
+
Message

而不是只看到:

Event ID = 1234

就直接下結論。

Event Level 有哪些?

Windows Event 常見 Level 大概可以看到:

Critical
Error
Warning
Information

在 Get-WinEvent 的 Level 數值裡,大致是:

Level 意義
1 Critical
2 Error
3 Warning
4 Information
5 Verbose

今天我們主要想找:

Critical
Error
Warning

也就是:

1
2
3
不要先抓全部再 Where-Object

一開始很容易寫成:

Get-WinEvent -LogName System |
Where-Object {
$_.LevelDisplayName -eq "Error"
}

這樣不是不能用。

但如果 Event Log 很大,就代表:

先把大量 Event 讀進來

再交給 Where-Object 篩選

效率通常不是很好。

Get-WinEvent 本身提供:

-FilterHashtable

可以讓我們在讀取時就先限定條件。

FilterHashtable

例如只找 System Log 裡面的 Error:

Get-WinEvent `
-FilterHashtable @{
LogName = "System"
Level = 2
}

這裡:

@{
}

叫做:

Hashtable

可以先把它理解成:

一組「欄位 = 條件」。

例如:

@{
LogName = "System"
Level = 2
}

就是:

LogName 必須是 System
而且
Level 必須是 Error
我們真正想要的是最近 24 小時

如果 Server 已經跑半年,直接抓所有 Error 一樣沒有太大意義。

我們現在比較想知道:

最近一天發生了什麼?

先建立:

$StartTime = (Get-Date).AddHours(-24)

如果現在:

2026/09/17 05:30

那 $StartTime 就會是:

2026/09/16 05:30

接著:

Get-WinEvent `
-FilterHashtable @{
LogName = "System"
StartTime = $StartTime
}

這樣就只會抓最近 24 小時。

再加入 Critical、Error、Warning

可以:

$Events = Get-WinEvent `
-FilterHashtable @{
LogName = "System"
StartTime = $StartTime
Level = 1, 2, 3
}

現在:

$Events

就是:

最近 24 小時
+
System Log
+
Critical / Error / Warning

這已經很接近每日巡檢需要的資料。

只留下我們真的想看的欄位

直接輸出 Event Object 還是有很多資訊。

所以使用:

$Events |
Select-Object `
TimeCreated,
Id,
LevelDisplayName,
ProviderName,
Message

結果大概會變成:

TimeCreated
Id
LevelDisplayName
ProviderName
Message

這幾個欄位基本上已經很夠我們做第一輪判斷。

先看看最近 24 小時有幾筆異常事件

可以:

$Events.Count

假設結果:

48

代表:

最近 24 小時共有 48 筆 Critical、Error 或 Warning Event。

但這個數字本身其實還不能直接代表 Server 很糟。

因為可能:

同一個 Warning
每隔幾分鐘一直重複出現

所以接下來更有用的是:

哪些 Event ID 出現最多次?

找出最常發生的 Event ID

可以使用:

$Events |
Group-Object Id

這裡第一次使用:

Group-Object

它會把相同的資料分在一起。

例如原本:

Event ID 10016
Event ID 10016
Event ID 7031
Event ID 10016
Event ID 55
Event ID 7031

經過:

Group-Object Id

之後會變成:

Count Name


3 10016
2 7031
1 55

這就非常實用。

因為比起看到:

今天有 50 個 Warning。

我更想知道:

是哪個 Warning 重複了 40 次?

再按照次數排序

可以:

$Events |
Group-Object Id |
Sort-Object Count -Descending

結果:

Count Name


35 10016
8 7031
3 55
2 41

現在最值得先看的可能就是:

10016 → 35 次

不過跟前面說的一樣:

Event ID 最好不要脫離 ProviderName 單獨判斷。

更實用:Event ID + Provider 一起 Group

可以建立自訂資料:

$EventSummary = $Events |
Group-Object Id, ProviderName |
Sort-Object Count -Descending

再看:

$EventSummary

可能:

Count Name


35 10016, Microsoft-Windows-DistributedCOM
8 7031, Service Control Manager
3 55, Ntfs

這就比單純看 Event ID 更有意義。

Warning 不一定代表故障

這一點跟前幾天一樣。

假設你打開一台 Windows Server:

Warning 30 筆

不能直接說:

Server 有 30 個問題。

例如有些環境可能長期存在:

DistributedCOM Warning

但實際上服務完全正常。

所以我們真正應該注意的是:

Critical
新的 Error
短時間大量重複
跟使用者回報時間吻合的 Event
重要 Service 相關 Error
Disk / NTFS / Hardware 類型 Event

因此 Event Log 自動化的目的不是:

所有 Warning 都通知工程師。

而是:

先幫工程師把值得看的東西整理出來。

先把 Critical 和 Error 分開計算

例如:

$CriticalEvents = $Events |
Where-Object {
$_.LevelDisplayName -eq "Critical"
}

$ErrorEvents = $Events |
Where-Object {
$_.LevelDisplayName -eq "Error"
}

$WarningEvents = $Events |
Where-Object {
$_.LevelDisplayName -eq "Warning"
}

接著:

$CriticalCount = $CriticalEvents.Count
$ErrorCount = $ErrorEvents.Count
$WarningCount = $WarningEvents.Count

最後:

Critical : 0
Error : 5
Warning : 42

第一眼就比:

Total Events = 47

容易理解很多。

Application Log 也用同一套方式

除了:

System

應用程式問題通常會進:

Application

所以只需要把:

LogName = "System"

改成:

LogName = "Application"

例如:

$ApplicationEvents = Get-WinEvent `
-FilterHashtable @{
LogName = "Application"
StartTime = $StartTime
Level = 1, 2, 3
}

同樣可以:

$ApplicationEvents |
Select-Object `
TimeCreated,
Id,
LevelDisplayName,
ProviderName,
Message

這樣我們就同時有:

System Events
Application Events
如果最近完全沒有 Event 呢?

這裡會遇到一個實際的小問題。

假設:

Get-WinEvent

找不到符合條件的 Event,有時候可能產生錯誤訊息。

所以可以配合 Day 6 學到的:

try
catch

例如:

try {

$SystemEvents = Get-WinEvent `
    -FilterHashtable @{
        LogName   = "System"
        StartTime = $StartTime
        Level     = 1, 2, 3
    } `
    -ErrorAction Stop

}
catch {

$SystemEvents = @()

}

這樣即使沒有符合條件的事件,後面的 Script 還是可以繼續。

但 catch 不一定代表系統真的錯了

這裡也要小心。

有些狀況是:

真的無法讀 Event Log

有些則只是:

沒有任何符合條件的 Event

正式 Script 最好還是把錯誤:

$_.Exception.Message

留下來。

例如:

catch {

$SystemEvents = @()

$EventReadError = $_.Exception.Message

}

這樣後面才知道:

是真的零筆,還是讀取過程出問題。

做一份 Event Summary

接著把結果做成:

$EventSummary = [PSCustomObject]@{

ComputerName = $env:COMPUTERNAME

CheckTime = Get-Date

StartTime = $StartTime

CriticalCount = (
    $SystemEvents |
    Where-Object LevelDisplayName -eq "Critical"
).Count

ErrorCount = (
    $SystemEvents |
    Where-Object LevelDisplayName -eq "Error"
).Count

WarningCount = (
    $SystemEvents |
    Where-Object LevelDisplayName -eq "Warning"
).Count

}

例如:

ComputerName : SERVER01
CheckTime : 2026/09/17 06:00
StartTime : 2026/09/16 06:00
CriticalCount : 0
ErrorCount : 3
WarningCount : 28

這就是很基本的:

Event Log Daily Summary

哪些 Event 應該優先注意?

今天我們可以先定一個很簡單的規則:

有 Critical
→ Critical

沒有 Critical,但有 Error
→ Warning

只有 Warning
→ Review

完全沒有
→ Normal

例如:

if ($CriticalCount -gt 0) {

$EventStatus = "Critical"

}
elseif ($ErrorCount -gt 0) {

$EventStatus = "Warning"

}
elseif ($WarningCount -gt 0) {

$EventStatus = "Review"

}
else {

$EventStatus = "Normal"

}

不過這裡一樣只是 Demo 邏輯。

正式環境不建議:

只要有一筆 Error,整台 Server 就一定是 Warning。

因為很多 Server 可能存在已知、無影響的 Event。

後面可以再慢慢加入:

Ignore List
Known Event
Critical Event ID
Provider Filter

讓規則更精準。

建立 Event Detail Report

除了 Summary,我們還是要保留明細。

可以:

$EventDetails = $SystemEvents |
Select-Object `
@{Name="ComputerName"; Expression={$env:COMPUTERNAME}},
TimeCreated,
Id,
LevelDisplayName,
ProviderName,
Message

這時:

$EventDetails

就會是一份完整的事件清單。

例如:

ComputerName TimeCreated ID Level Provider
SERVER01 05:23 7031 Error Service Control Manager
SERVER01 05:17 10016 Warning DistributedCOM
SERVER01 04:58 55 Error Ntfs
Message 可能很長

Event Log 的:

Message

有時候非常長。

放進 CSV 沒問題,但如果只是要做簡單摘要,可能不好閱讀。

所以我們可以保留完整 Message 在:

Event_Detail.csv

Summary 則只顯示:

Count
Event ID
Provider
Level

這樣比較合理。

也就是:

Summary
→ 快速看問題

Detail
→ 真正排查時使用

這個觀念跟前面的:

Server Summary
Disk Detail
Service Detail

其實是一樣的。

找出發生最多次的事件

再建立一份:

$TopEvents = $SystemEvents |
Group-Object Id, ProviderName |
Sort-Object Count -Descending |
Select-Object -First 10

例如:

Count Name


31 10016, Microsoft-Windows-DistributedCOM
7 7031, Service Control Manager
4 55, Ntfs

這份資料其實非常適合每日巡檢。

因為工程師不用從幾百筆 Event 中找:

哪一類事件一直重複?

PowerShell 直接幫我們整理好了。

讓 TopEvents 更適合 CSV

Group-Object 的結果直接輸出並不是最漂亮。

可以再轉一次:

$TopEvents = $SystemEvents |
Group-Object Id, ProviderName |
Sort-Object Count -Descending |
Select-Object -First 10 |
ForEach-Object {

$FirstEvent = $_.Group[0]

[PSCustomObject]@{

    ComputerName = $env:COMPUTERNAME

    EventID = $FirstEvent.Id

    Provider = $FirstEvent.ProviderName

    Level = $FirstEvent.LevelDisplayName

    Count = $_.Count

}

}

結果變成:

ComputerName EventID Provider Level Count
SERVER01 10016 DistributedCOM Warning 31
SERVER01 7031 Service Control Manager Error 7
SERVER01 55 Ntfs Error 4

這就很好拿去做報表。

今天完整 Script

把今天內容整理成一支簡單的 Event Log 巡檢:

==========================================

Windows Event Log Daily Health Check

==========================================

$ComputerName = $env:COMPUTERNAME

$CheckTime = Get-Date

$StartTime = (Get-Date).AddHours(-24)

$ReportFolder = "C:\Temp"

$Date = Get-Date -Format "yyyyMMdd"

==========================================

Create Report Folder

==========================================

if (-not (Test-Path $ReportFolder)) {

New-Item `
    -Path $ReportFolder `
    -ItemType Directory |
    Out-Null

}

==========================================

Read System Event Log

==========================================

try {

$SystemEvents = Get-WinEvent `
    -FilterHashtable @{
        LogName   = "System"
        StartTime = $StartTime
        Level     = 1, 2, 3
    } `
    -ErrorAction Stop

$EventReadError = ""

}
catch {

$SystemEvents = @()

$EventReadError = $_.Exception.Message

}

==========================================

Event Count

==========================================

$CriticalCount = (
$SystemEvents |
Where-Object {
$_.LevelDisplayName -eq "Critical"
}
).Count

$ErrorCount = (
$SystemEvents |
Where-Object {
$_.LevelDisplayName -eq "Error"
}
).Count

$WarningCount = (
$SystemEvents |
Where-Object {
$_.LevelDisplayName -eq "Warning"
}
).Count

==========================================

Event Status

==========================================

if ($CriticalCount -gt 0) {

$EventStatus = "Critical"

}
elseif ($ErrorCount -gt 0) {

$EventStatus = "Warning"

}
elseif ($WarningCount -gt 0) {

$EventStatus = "Review"

}
else {

$EventStatus = "Normal"

}

==========================================

Summary

==========================================

$Summary = [PSCustomObject]@{

ComputerName  = $ComputerName

CheckTime     = $CheckTime

StartTime     = $StartTime

CriticalCount = $CriticalCount

ErrorCount    = $ErrorCount

WarningCount  = $WarningCount

EventStatus   = $EventStatus

ReadError     = $EventReadError

}

==========================================

Event Detail

==========================================

$EventDetails = $SystemEvents |
Select-Object `
@{Name="ComputerName"; Expression={$ComputerName}},
TimeCreated,
Id,
LevelDisplayName,
ProviderName,
Message

==========================================

Top Events

==========================================

$TopEvents = $SystemEvents |
Group-Object Id, ProviderName |
Sort-Object Count -Descending |
Select-Object -First 10 |
ForEach-Object {

$FirstEvent = $_.Group[0]

[PSCustomObject]@{

    ComputerName = $ComputerName

    EventID      = $FirstEvent.Id

    Provider     = $FirstEvent.ProviderName

    Level        = $FirstEvent.LevelDisplayName

    Count        = $_.Count

}

}

==========================================

Export CSV

==========================================

$Summary |
Export-Csv -Path "$ReportFolder\Event_Summary_$Date.csv"
-NoTypeInformation `
-Encoding UTF8

$EventDetails |
Export-Csv -Path "$ReportFolder\Event_Detail_$Date.csv"
-NoTypeInformation `
-Encoding UTF8

$TopEvents |
Export-Csv -Path "$ReportFolder\Event_Top10_$Date.csv"
-NoTypeInformation `
-Encoding UTF8

==========================================

Display

==========================================

Write-Host ""
Write-Host "===== Event Log Summary ====="
Write-Host ""

$Summary

Write-Host ""
Write-Host "===== Top Events ====="
Write-Host ""

$TopEvents
執行後會得到三份報表
C:\Temp

├── Event_Summary_20260917.csv

├── Event_Detail_20260917.csv

└── Event_Top10_20260917.csv

三份報表分別負責不同用途。

Summary
這台 Server 今天有沒有值得注意的 Event?

例如:

Critical : 0
Error : 3
Warning : 28
Status : Warning
Detail

真正排查時看:

發生時間
Event ID
Provider
Level
Message
Top 10

回答:

今天哪一種 Event 出現最多次?

有 Error 時,不要急著直接修

假設看到:

Event ID 7031
Service Control Manager
Error

第一步最好不是:

看到 Service Error → 直接 Restart Service。

比較合理的排查方式是:

Event 發生什麼?

哪個 Provider?

Event ID?

Message?

發生時間?

同時間還有其他 Event 嗎?

Service 現在狀態?

使用者是否有影響?

Event Log 是:

證據。

不是單看到紅色 Error 就代表我們已經知道 Root Cause。

Event 發生時間非常重要

例如使用者說:

「昨天晚上 10 點左右系統斷掉 5 分鐘。」

這時候比起直接:

找今天所有 Error

更有效的方法是:

21:50
~
22:10

只看那段時間。

PowerShell 可以:

$StartTime = Get-Date "2026-09-16 21:50"

$EndTime = Get-Date "2026-09-16 22:10"

然後:

Get-WinEvent `
-FilterHashtable @{
LogName = "System"
StartTime = $StartTime
EndTime = $EndTime
}

這也是 Event Log 排查很重要的思維:

先把時間範圍縮小。

而不是直接在幾十萬筆 Event 裡找問題。

System 和 Application 不要混在一起看

一般來說,我自己會先這樣區分:

System

├── Driver
├── Service
├── Disk
├── Network
├── Windows
└── Hardware / OS

Application

├── Application Error
├── .NET
├── SQL
├── IIS Application
└── 各種應用程式

當然實際情況不一定完全這樣。

但至少遇到問題時可以先想:

這比較像 Windows / 系統層問題,還是應用程式問題?

這樣會比全部混在一起找快很多。

我們的 Server Health Check 又多一層了

到 Day 9,目前已經有:

            Windows Server
                  │
     ┌────────────┼────────────┐
     │            │            │
    CPU         Memory        Disk
     │            │            │
     └────────────┼────────────┘
                  │
               Service
                  │
               Uptime
                  │
             Event Log
                  │
                  ▼
            Health Check

前面的:

CPU / Memory / Disk

比較像:

現在的狀態。

而 Event Log 則開始幫我們回答:

最近發生過什麼事情?

這兩種資料合起來之後,巡檢的價值會高很多。

Day 9 小結

今天主要使用:

Get-WinEvent

並學會用:

-FilterHashtable

縮小 Event 範圍:

@{
LogName = "System"
StartTime = $StartTime
Level = 1, 2, 3
}

再搭配:

Group-Object
Sort-Object
Select-Object
ForEach-Object

把大量 Event 變成:

最近 24 小時有多少 Critical?
有多少 Error?
有多少 Warning?

哪個 Event 發生最多?
是哪個 Provider?
發生在什麼時間?

所以今天真正做的不是:

用 PowerShell 取代 Event Viewer。

而是:

先用 PowerShell 把大量 Event Log 縮小成值得工程師看的範圍。

這對維運工作來說,比單純把所有 Error 全部倒出來實際得多。

Day 10 預告
Day 10|從單台走向多台:PowerShell Remoting 批次巡檢 Windows Server

到 Day 9 為止,我們已經可以檢查:

CPU
Memory
Disk
Service
Uptime
Event Log

但現在還有一個很大的問題:

這些 Script 主要都還是在本機執行。

如果公司有:

SERVER01
SERVER02
SERVER03
SERVER04
...
SERVER50

我們不可能:

RDP SERVER01
執行 Script

RDP SERVER02
再執行一次

RDP SERVER03
再執行一次

所以 Day 10 會正式進入:

Invoke-Command

New-PSSession

Test-WSMan

並開始理解:

Management Server

├──── SERVER01
├──── SERVER02
├──── SERVER03
└──── SERVER04

讓一台管理端電腦可以批次取得其他 Windows Server 的狀態。

從 Day 10 開始,我們前面寫的 Health Check 才會真正從「單機 Script」開始變成「企業環境可以擴充的巡檢工具」。


上一篇
Day 8|Windows Server 巡檢進階:Service、Uptime 與最後開機時間
下一篇
Day 10|從單台走向多台:PowerShell Remoting 批次巡檢 Windows Server
系列文
系統工程師的 30 天自動化維運實戰:PowerShell × AD × Windows Server11
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言